//
//  WCFindInProjectWindowController.m
//  WabbitCode
//
//  Created by William Towe on 3/9/09.
//  Copyright 2009 Revolution Software. All rights reserved.
//

#import "WCFindInProjectWindowController.h"
#import "WCInterfacePerformer.h"
#import "PSMTabBarControl+WCExtensions.h"
#import "WCTextFile.h"
#import "WCProjectGroup.h"
#import "WCMacros.h"
#import <RegexKit/RegexKit.h>
#import "WCFindResult.h"
#import "WCProject.h"
#import "WCBadgedIconAndTextFieldCell.h"
#import "NSArray+WCExtensions.h"
#import "NSString+WCExtensions.h"
#import "NSDictionary+WCExtensions.h"
#import "MGScopeBar.h"
#import "NSTextView+WCExtensions.h"
#import "WCSyntaxHighlighter.h"
#import "WCAppController.h"
#import "WCSymbol.h"
#import "WCBasicPerformer.h"


NSString *kWCProjectPrefsFindInProjectIgnoreCaseKey = @"kWCProjectPrefsFindInProjectIgnoreCaseKey";
NSString *kWCProjectPrefsFindInProjectUseRegularExpressionsKey = @"kWCProjectPrefsFindInProjectUseRegularExpressionsKey";
NSString *kWCProjectPrefsFindInProjectWindowFrameKey = @"kWCProjectPrefsFindInProjectWindowFrameKey";
NSString *kWCProjectPrefsFindInProjectSplitViewDividerPositionKey = @"kWCProjectPrefsFindInProjectSplitViewDividerPositionKey";
NSString *kWCProjectPrefsFindInProjectFindRecentsKey = @"kWCProjectPrefsFindInProjectFindRecentsKey";
NSString *kWCProjectPrefsFindInProjectReplaceRecentsKey = @"kWCProjectPrefsFindInProjectReplaceRecentsKey";
NSString *kWCProjectPrefsFindInProjectScopeIdentifierKey = @"kWCProjectPrefsFindInProjectScopeIdentifierKey";

#define kWCBuildResultsWindowToolbarIdentifier @"org.revsoft.wabbitcode.findinprojecttoolbar"

#define kWCFindInProjectMaxRecents 15

@interface WCFindInProjectWindowController (Private)
- (BOOL)_canPerformReplaceAction;
- (void)_performReplaceAction;
- (void)_loadProjectPreferences;
- (void)_findActionThreaded:(NSDictionary *)attributes;
- (void)_addFindResultOnMainThread:(NSDictionary *)parentAndChildDict;
- (void)_toggleIsProcessingOnMainThread;
- (void)_setProjectInfoStringOnMainThread:(NSString *)string;
- (void)_expandOutlineViewOnMainThread;
- (void)_updateOutlineViewOnMainThread;
- (void)_updateFindProgressOnMainThread:(NSNumber *)currentProgress;
@end

@implementation WCFindInProjectWindowController
#pragma mark *** Subclass Overrides ***
- (id)init {
	return [self initWithWindowNibName:@"WCFindInProjectWindow"];
}

- (void)dealloc {
	[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.kWCPrefsTabsAnimateAddingAndRemovingKey"];
	[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:@"values.kWCPrefsTabsAllowScrubbingOfTabsKey"];
	
	[_findString release];
	[_findResult release];
	[_replaceRecents release];
	[_findRecents release];
	[_replaceString release];
	[_findLastString release];
	[super dealloc];
}

- (void)windowDidLoad {
	[super windowDidLoad];
	
	NSToolbar *toolbar = [[[NSToolbar alloc] initWithIdentifier:kWCBuildResultsWindowToolbarIdentifier] autorelease];
	
#ifndef WCDEBUG
	[toolbar setAutosavesConfiguration:YES];
#endif
	[toolbar setDisplayMode:NSToolbarDisplayModeDefault];
	[toolbar setSizeMode:NSToolbarSizeModeRegular];
	[toolbar setAllowsUserCustomization:YES];
	
	[toolbar setDelegate:self];
	[[self window] setToolbar:toolbar];
	
	[_psmTabBarControl performSetup];
	
	[_textViewActionButton setMenu:[[WCInterfacePerformer sharedInstance] menuForProjectTextViewActionButton]];
	
	[_projectBuildStatusTextField bind:@"imagesAndStrings" toObject:[self project] withKeyPath:@"buildStatusImagesAndStrings" options:nil];
	
	[[_textViewSymbolsActionPopUpButton menu] setDelegate:self];
	
	[_outlineView setTarget:self];
	[_outlineView setDoubleAction:@selector(_outlineViewDoubleAction:)];
	
	[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.kWCPrefsTabsAnimateAddingAndRemovingKey" options:NSKeyValueObservingOptionNew context:(void *)kWCPrefsTabsAnimateAddingAndRemovingKey];
	[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.kWCPrefsTabsAllowScrubbingOfTabsKey" options:NSKeyValueObservingOptionNew context:(void *)kWCPrefsTabsAllowScrubbingOfTabsKey];
	
	// load the find in project settings
	[self _loadProjectPreferences];
}

- (NSString *)windowTitleForDocumentDisplayName:(NSString *)displayName {
	return [NSString stringWithFormat:NSLocalizedString(@"%@ - Find in Project",@"%@ - Find in Project"), [displayName stringByDeletingPathExtension]];
}
#pragma mark NSToolbar Delegate
- (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)identifier willBeInsertedIntoToolbar:(BOOL)flag {
	NSToolbarItem *item = [[WCBasicPerformer sharedInstance] toolbarItemForItemIdentifier:identifier inProject:[self project]];
	
	return item;
}

- (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar {
	return [NSArray arrayWithObjects:kWCProjectToolbarProjectOverviewIdentifier,kWCProjectToolbarBuildIdentifier,kWCProjectToolbarProjectWindowIdentifier, NSToolbarSpaceItemIdentifier, NSToolbarFlexibleSpaceItemIdentifier, NSToolbarSeparatorItemIdentifier, NSToolbarCustomizeToolbarItemIdentifier, nil];
}

- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar {
	return [NSArray arrayWithObjects:kWCProjectToolbarProjectOverviewIdentifier,NSToolbarFlexibleSpaceItemIdentifier,kWCProjectToolbarBuildIdentifier,NSToolbarFlexibleSpaceItemIdentifier,kWCProjectToolbarProjectWindowIdentifier, nil];
}
#pragma mark NSSplitView Delegate
- (CGFloat)splitView:(NSSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMin ofSubviewAt:(NSInteger)offset {
	if (![splitView isVertical])
		return proposedMin + 150.0;
	return proposedMin;
}

- (CGFloat)splitView:(NSSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMax ofSubviewAt:(NSInteger)offset {
	if (![splitView isVertical])
		return proposedMax - 150.0;
	return proposedMax;
}

- (void)splitView:(NSSplitView *)splitView resizeSubviewsWithOldSize:(NSSize)oldSize {
	if ([splitView inLiveResize] && ![splitView isVertical]) {
		// keep the top view the same while resizing the bottom view
		NSArray *subviews = [splitView subviews];
		NSView *topView = [subviews objectAtIndex:0];
		NSView *bottomView = [subviews lastObject];
		NSRect currentFrame = [splitView frame];
		NSRect topFrame = [topView frame];
		NSRect bottomFrame = [bottomView frame];
		CGFloat dividerThickness = [splitView dividerThickness];
		
		topFrame.size.width = currentFrame.size.width;
		bottomFrame.size.height = currentFrame.size.height - topFrame.size.height - dividerThickness;
		bottomFrame.size.width = currentFrame.size.width;
		bottomFrame.origin.y = topFrame.size.height + dividerThickness;
		
		[topView setFrame:topFrame];
		[bottomView setFrame:bottomFrame];
	}
	else {
		[splitView adjustSubviews];
	}
}
#pragma mark NSOutlineView Delegate
- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item {
	id object = [item representedObject];
	
	[cell setIcon:[object icon]];
	[cell setIconSize:NSMakeSize(16.0, 16.0)];
	[cell setBadgeCount:[object badgeCount]];
	
	if ([object resultType] != WCFRTypeMatch)
		return;
	
	[cell setAttributedStringValue:[object resultAttributedString]];
}

/*
- (BOOL)outlineView:(NSOutlineView *)outlineView isGroupItem:(id)item {
	return (![[item representedObject] isLeaf]);
}
 */
#pragma mark NSMenu Delegate
- (NSInteger)numberOfItemsInMenu:(NSMenu *)menu {
	NSUInteger count = [[self currentTextFile] numberOfFileSymbols];
	return (!count)?2:++count;
}

- (BOOL)menu:(NSMenu *)menu updateItem:(NSMenuItem *)item atIndex:(NSInteger)index shouldCancel:(BOOL)shouldCancel {
	NSUInteger count = [[self currentTextFile] numberOfFileSymbols];
	[item setTarget:self];
	[item setAction:@selector(_textViewSymbolsPopUpButtonAction:)];
	if (!count) {
		if (!index) {
			[item setHidden:YES];
			[item setImage:[NSImage imageNamed:NSImageNameListViewTemplate]];
		}
		else {
			[item setTitle:NSLocalizedString(@"No Symbols",@"No Symbols")];
		}
	}
	else {
		if (!index) {
			[item setHidden:YES];
			[item setImage:[NSImage imageNamed:NSImageNameListViewTemplate]];
		}
		else {
			WCSymbol *symbol = [[[self currentTextFile] fileSymbolsArray] objectAtIndex:--index];
			[item setTitle:[NSString stringWithFormat:@"%@ - Line: %u",[symbol name],[symbol lineNumber]+1]];
			[item setImage:[symbol icon]];
			[item setRepresentedObject:symbol];
		}
	}
	return YES;
}
#pragma mark NSKeyValueObserving Protocol
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
	if ([(NSString *)context isEqualToString:kWCPrefsTabsAnimateAddingAndRemovingKey]) {
		[[self psmTabBarControl] setAutomaticallyAnimates:NSUserDefaultsBoolForKey(kWCPrefsTabsAnimateAddingAndRemovingKey)];
	}
	else if ([(NSString *)context isEqualToString:kWCPrefsTabsAllowScrubbingOfTabsKey]) {
		[[self psmTabBarControl] setAllowsScrubbing:NSUserDefaultsBoolForKey(kWCPrefsTabsAllowScrubbingOfTabsKey)];
	}
	/*
	 else if ([(NSString *)context isEqualToString:@"fileURL"]) {
	 [[self projectOutlineView] setNeedsDisplay:YES];
	 [[self fileDetailTableView] setNeedsDisplay:YES];
	 }
	 */
	else {
		[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
	}
}
#pragma mark NSUserInterfaceValidations Protocol
- (BOOL)validateUserInterfaceItem:(id < NSValidatedUserInterfaceItem >)item; {
	SEL action = [item action];
	
	if (action == @selector(_textViewSymbolsPopUpButtonAction:)) {
		if (![self currentTextFile])
			return NO;
	}
	return YES;
}
#pragma mark PSMTabBarControl Delegate
- (BOOL)tabView:(NSTabView *)aTabView shouldCloseTabViewItem:(NSTabViewItem *)item; {
	return [[self project] canRemoveTabViewItem:item inTabViewContext:self];
}
- (void)tabView:(NSTabView *)aTabView didCloseTabViewItem:(NSTabViewItem *)item; {
	[[self project] removeTabViewItem:item inTabViewContext:self saveTextFile:NO];
	if ([self currentTextView]) {
		[_lineColumnButton bind:@"title" toObject:[self currentTextView] withKeyPath:@"infoString" options:nil];
	}
	else {
		[_lineColumnButton unbind:@"title"];
		[_lineColumnButton setTitle:NSLocalizedString(@"N/A",@"N/A")];
		[_textViewSymbolsActionPopUpButton removeAllItems];
		NSMenuItem *item = [[[NSMenuItem alloc] initWithTitle:@"" action:NULL keyEquivalent:@""] autorelease];
		[item setImage:[NSImage imageNamed:NSImageNameListViewTemplate]];
		[[_textViewSymbolsActionPopUpButton menu] addItem:item];
	}
}

- (void)tabView:(NSTabView *)tabView didSelectTabViewItem:(NSTabViewItem *)item {
	[_lineColumnButton bind:@"title" toObject:[[item view] documentView] withKeyPath:@"infoString" options:nil];;
}

- (BOOL)tabView:(NSTabView *)tabView shouldDragTabViewItem:(NSTabViewItem *)tabViewItem fromTabBar:(PSMTabBarControl *)tabBarControl; {
	return ([tabView numberOfTabViewItems] == 1)?NO:YES;
}
- (BOOL)tabView:(NSTabView *)tabView shouldDropTabViewItem:(NSTabViewItem *)tabViewItem inTabBar:(PSMTabBarControl *)tabBarControl; {
	if ([[tabBarControl tabView] isEqualTo:tabView])
		return YES;
	return NO;
}
- (BOOL)tabView:(NSTabView *)tabView shouldAllowTabViewItem:(NSTabViewItem *)tabViewItem toLeaveTabBar:(PSMTabBarControl *)tabBarControl; {
	return NO;
}

#pragma mark *** Public Methods ***
#pragma mark MGScopeBarDelegate Protocol
- (int)numberOfGroupsInScopeBar:(MGScopeBar *)theScopeBar; {
	return 1;
}

- (NSArray *)scopeBar:(MGScopeBar *)theScopeBar itemIdentifiersForGroup:(int)groupNumber; {
	return [NSArray arrayWithObjects:kWCFindInProjectCurrentFileIdentifier, kWCFindInProjectOpenFilesIdentifier, kWCFindInProjectAllFilesIdentifier, nil];
}

- (NSString *)scopeBar:(MGScopeBar *)theScopeBar labelForGroup:(int)groupNumber; {
	return NSLocalizedString(@"Search:",@"Search:");
}

- (MGScopeBarGroupSelectionMode)scopeBar:(MGScopeBar *)theScopeBar selectionModeForGroup:(int)groupNumber; {
	return MGRadioSelectionMode;
}

- (NSString *)scopeBar:(MGScopeBar *)theScopeBar titleOfItem:(NSString *)identifier inGroup:(int)groupNumber; {
	return identifier;
}
#pragma mark Accessors
@synthesize psmTabBarControl=_psmTabBarControl,tabView=_tabView,findString=_findString,ignoreCase=_ignoreCase,useRegularExpressions=_useRegularExpressions,projectBuildStatusTextField=_projectBuildStatusTextField,projectStatusTextField=_projectStatusTextField,findResult=_findResult,splitView=_splitView,replaceString=_replaceString,findRecents=_findRecents,replaceRecents=_replaceRecents,mgScopeBar=_mgScopeBar,numberOfFindResults=_numberOfFindResults,findLastString=_findLastString,shouldCancelFindAction=_shouldCancelFindAction,findProgressTotal=_findProgressTotal,findProgress=_findProgress;

@dynamic scopeIdentifier;
- (NSString *)scopeIdentifier {
	return [[[_mgScopeBar selectedItems] firstObject] firstObject];
}
- (void)setScopeIdentifier:(NSString *)value {
	[_mgScopeBar setSelected:YES forItem:value inGroup:0];
}

@dynamic project;
- (WCProject *)project {
	return (WCProject *)[self document];
}

@dynamic isProcessing;
- (BOOL)isProcessing {
	return _fipFlags.isProcessing;
}
- (void)setIsProcessing:(BOOL)flag {
	_fipFlags.isProcessing = flag;
}
#pragma mark IBActions

- (IBAction)findAction:(id)sender; {
	NSString *findString = [self findString];
	
	if (![findString length]) {
		NSBeep();
		return;
	}
	else if ([self isProcessing]) {
		[self setShouldCancelFindAction:YES];
		return;
	}
	
	NSString *scopeIdentifier = [[[_mgScopeBar selectedItems] firstObject] firstObject];
	NSArray *textFiles = nil;
	
	if ([scopeIdentifier isEqualToString:kWCFindInProjectCurrentFileIdentifier]) {
		textFiles = ([self currentTextFile])?[NSArray arrayWithObject:[self currentTextFile]]:[NSArray array];
	}
	else if ([scopeIdentifier isEqualToString:kWCFindInProjectOpenFilesIdentifier]) {
		textFiles = [[[self project] openTextFiles] allObjects];
	}
	else if ([scopeIdentifier isEqualToString:kWCFindInProjectAllFilesIdentifier]) {
		textFiles = [[[self project] projectGroup] textFiles];
	}
	
	if (![textFiles count]) {
		NSBeep();
		return;
	}
	
	BOOL ignoreCase = [self ignoreCase];
	BOOL useRegularExpressions = [self useRegularExpressions];
	
	const RKCompileOption options = (ignoreCase)?(RKCompileMultiline|RKCompileCaseless):(RKCompileMultiline);
	
	if (useRegularExpressions) {
		// always check to make sure a valid regex pattern has been given
		if (![RKRegex isValidRegexString:findString options:options]) {
			NSAlert *alert = [NSAlert alertWithMessageText:[NSString stringWithFormat:NSLocalizedString(@"%@ is not a valid regular expression",@"%@ is not a valid regular expression"), findString] defaultButton:NSLocalizedButtonOK alternateButton:nil otherButton:nil informativeTextWithFormat:nil];
			[alert beginSheetModalForWindow:[self window] modalDelegate:nil didEndSelector:NULL contextInfo:NULL];
			return;
		}
	}
	
	NSMutableArray *newRecents = [NSMutableArray arrayWithArray:[self findRecents]];
	
	if (![newRecents containsObject:findString])
		[newRecents insertObject:findString atIndex:0];
	
	if ([newRecents count] > kWCFindInProjectMaxRecents)
		[newRecents removeLastObject];
	
	[self setFindRecents:newRecents];
	
	[self setIsProcessing:YES];
	[self setFindProgressTotal:(CGFloat)[textFiles count]];
	
	if ([self useRegularExpressions])
		[self setFindResult:[WCFindResult findResultWithType:WCFRTypeMessage textFile:(WCTextFile *)[[self project] projectGroup] range:NSMakeRange(0, 0) message:[NSString stringWithFormat:@"Searching for strings matching \"%@\" in %u files", [self findString], [textFiles count]]]];
	else
		[self setFindResult:[WCFindResult findResultWithType:WCFRTypeMessage textFile:(WCTextFile *)[[self project] projectGroup] range:NSMakeRange(0, 0) message:[NSString stringWithFormat:@"Searching for \"%@\" in %u files", [self findString], [textFiles count]]]];
	
	[_outlineView expandItem:[_outlineView itemAtRow:0]];
	
	[NSThread detachNewThreadSelector:@selector(_findActionThreaded:) toTarget:self withObject:[NSDictionary dictionaryWithObjectsAndKeys:scopeIdentifier, @"scopeIdentifier", textFiles, @"textFiles", NSNumberWithBool(useRegularExpressions), @"useRegularExpressions", NSNumberWithBool(ignoreCase), @"ignoreCase", findString, @"findString", [self findResult], @"findResult", nil]];
}

- (IBAction)replaceAction:(id)sender; {
	if (![self _canPerformReplaceAction])
		return;
	
	NSAlert *alert = nil;
	
	if (![[self replaceString] length])
		alert = [NSAlert alertWithMessageText:[NSString stringWithFormat:NSLocalizedString(@"Delete \"%@\"",@"Replace \"%@\""), [self findLastString]] defaultButton:NSLocalizedString(@"Delete",@"Delete") alternateButton:NSLocalizedButtonCancel otherButton:nil informativeTextWithFormat:[NSString stringWithFormat:NSLocalizedString(@"Do you really want to delete %u occurences of \"%@\"?",@"Informative text for find in project replace alert"), [self numberOfFindResults], [self findLastString]]];
	else
		alert = [NSAlert alertWithMessageText:[NSString stringWithFormat:NSLocalizedString(@"Replace \"%@\" with \"%@\"",@"Replace \"%@\" with \"%@\""), [self findLastString], [self replaceString]] defaultButton:NSLocalizedString(@"Replace",@"Replace") alternateButton:NSLocalizedButtonCancel otherButton:nil informativeTextWithFormat:[NSString stringWithFormat:NSLocalizedString(@"Do you really want to replace %u occurences of \"%@\" with \"%@\"?",@"Informative text for find in project replace alert"), [self numberOfFindResults], [self findLastString], [self replaceString]]];
	
	[alert beginSheetModalForWindow:[self window] modalDelegate:self didEndSelector:@selector(_replaceAlertDidEnd:code:info:) contextInfo:NULL];
}
#pragma mark WCTabViewContext Protocol
- (WCTextFile *)currentTextFile; {
	return [[self currentTabViewItem] identifier];
}
- (WCTextView *)currentTextView; {
	return [[[self currentTabViewItem] view] documentView];
}
- (NSTabViewItem *)currentTabViewItem; {
	return [[self tabView] selectedTabViewItem];
}
- (NSUInteger)numberOfTabViewItems; {
	return [[self tabView] numberOfTabViewItems];
}
- (NSTabView *)tabView; {
	return _tabView;
}
- (PSMTabBarControl *)psmTabBarControl; {
	return _psmTabBarControl;
}
- (NSWindow *)contextWindow; {
	return [self window];
}
#pragma mark *** Private Methods ***
- (BOOL)_canPerformReplaceAction; {	
	if (![self numberOfFindResults]) {
		NSBeep();
		return NO;
	}
	
	if (![[self findLastString] length]) {
		NSBeep();
		return NO;
	}
	
	if (![[self replaceString] length] || [self isProcessing]) {
		[self setReplaceString:@""];
	}
	
	NSString *scopeIdentifier = [[[_mgScopeBar selectedItems] firstObject] firstObject];
	NSArray *textFiles = nil;
	
	if ([scopeIdentifier isEqualToString:kWCFindInProjectCurrentFileIdentifier]) {
		textFiles = ([self currentTextFile])?[NSArray arrayWithObject:[self currentTextFile]]:[NSArray array];
	}
	else if ([scopeIdentifier isEqualToString:kWCFindInProjectOpenFilesIdentifier]) {
		textFiles = [[[self project] openTextFiles] allObjects];
	}
	else if ([scopeIdentifier isEqualToString:kWCFindInProjectAllFilesIdentifier]) {
		textFiles = [[[self project] projectGroup] textFiles];
	}
	
	if (![textFiles count]) {
		NSBeep();
		return NO;
	}
	
	return YES;
}

- (void)_performReplaceAction; {
	NSString *findString = [self findLastString];
	NSString *replaceString = [self replaceString];
	
	NSString *scopeIdentifier = [[[_mgScopeBar selectedItems] firstObject] firstObject];
	NSArray *textFiles = nil;
	
	if ([scopeIdentifier isEqualToString:kWCFindInProjectCurrentFileIdentifier]) {
		textFiles = ([self currentTextFile])?[NSArray arrayWithObject:[self currentTextFile]]:[NSArray array];
	}
	else if ([scopeIdentifier isEqualToString:kWCFindInProjectOpenFilesIdentifier]) {
		textFiles = [[[self project] openTextFiles] allObjects];
	}
	else if ([scopeIdentifier isEqualToString:kWCFindInProjectAllFilesIdentifier]) {
		textFiles = [[[self project] projectGroup] textFiles];
	}
	
	NSMutableArray *newRecents = [NSMutableArray arrayWithArray:[self replaceRecents]];
	
	if (![newRecents containsObject:replaceString])
		[newRecents insertObject:replaceString atIndex:0];
	
	if ([newRecents count] > kWCFindInProjectMaxRecents)
		[newRecents removeLastObject];
	
	[self setReplaceRecents:newRecents];
	
	NSUInteger numberOfReplacements = 0;
	
	BOOL ignoreCase = [self ignoreCase];
	
	if ([self useRegularExpressions]) {
		const RKCompileOption options = (ignoreCase)?(RKCompileMultiline|RKCompileCaseless):(RKCompileMultiline);
		
		// always check to make sure a valid regex pattern has been given
		if (![RKRegex isValidRegexString:findString options:options]) {
			NSAlert *alert = [NSAlert alertWithMessageText:[NSString stringWithFormat:NSLocalizedString(@"%@ is not a valid regular expression",@"%@ is not a valid regular expression"), findString] defaultButton:NSLocalizedButtonOK alternateButton:nil otherButton:nil informativeTextWithFormat:nil];
			[alert beginSheetModalForWindow:[self window] modalDelegate:nil didEndSelector:NULL contextInfo:NULL];
			return;
		}
		
		[self setIsProcessing:YES];
		
		RKRegex *regex = [[[RKRegex alloc] initWithRegexString:findString options:options] autorelease];
		
		for (WCTextFile *tf in textFiles) {
			NSString *originalString = [[tf fileTextStorage] string];
			NSMutableString *string = [NSMutableString stringWithString:originalString];
			
			numberOfReplacements += [string match:regex replace:RKReplaceAll withString:replaceString];
			
			if (![originalString isEqualToString:string] && [originalString length]) {
				WCTextView *tv = [tf textViewForReplace];
				if ([tv shouldChangeTextInRange:[tv completeRange] replacementString:string]) {
					[tv replaceCharactersInRange:[tv completeRange] withString:string];
					[tv didChangeText];
				}
			}
		}
	}
	else {
		[self setIsProcessing:YES];
		
		for (WCTextFile *tf in textFiles) {
			NSString *originalString = [[tf fileTextStorage] string];
			NSMutableString *string = [NSMutableString stringWithString:originalString];
			NSRange searchRange = [string wholeRange];
			
			if (ignoreCase)
				numberOfReplacements += [string replaceOccurrencesOfString:findString withString:replaceString options:NSCaseInsensitiveSearch range:searchRange];
			else
				numberOfReplacements += [string replaceOccurrencesOfString:findString withString:replaceString options:NSLiteralSearch range:searchRange];
			
			if (![originalString isEqualToString:string] && [originalString length]) {
				WCTextView *tv = [tf textViewForReplace];
				if ([tv shouldChangeTextInRange:[tv completeRange] replacementString:string]) {
					[tv replaceCharactersInRange:[tv completeRange] withString:string];
					[tv didChangeText];
				}
			}
		}
	}
	
	[[self project] setProjectInfoString:[NSString stringWithFormat:NSLocalizedString(@"Replaced %u occurrences of \"%@\" with \"%@\"",@"Find in project replace operation finished"), numberOfReplacements, findString, replaceString]];
	[[[self project] projectOutlineView] setNeedsDisplay:YES];
	[self setIsProcessing:NO];
}

- (void)_loadProjectPreferences; {
	// ignore case
	[self setIgnoreCase:[[[self project] projectPreferences] boolForKey:kWCProjectPrefsFindInProjectIgnoreCaseKey]];
	
	// use regular expressions
	[self setUseRegularExpressions:[[[self project] projectPreferences] boolForKey:kWCProjectPrefsFindInProjectUseRegularExpressionsKey]];
	
	// window frame
	NSString *windowFrameAsString = [[[self project] projectPreferences] objectForKey:kWCProjectPrefsFindInProjectWindowFrameKey];
	if (windowFrameAsString)
		[[self window] setFrame:NSRectFromString(windowFrameAsString) display:NO];
	
	// split view divider position
	NSNumber *dividerPosition = [[[self project] projectPreferences] objectForKey:kWCProjectPrefsFindInProjectSplitViewDividerPositionKey];
	if (dividerPosition)
		[[self splitView] setPosition:[dividerPosition floatValue] ofDividerAtIndex:0];
	
	// find combo box recents
	NSArray *findRecents = [[[self project] projectPreferences] objectForKey:kWCProjectPrefsFindInProjectFindRecentsKey];
	if (findRecents)
		[self setFindRecents:findRecents];
	else
		[self setFindRecents:[NSArray array]];
	
	// replace combo box recents
	NSArray *replaceRecents = [[[self project] projectPreferences] objectForKey:kWCProjectPrefsFindInProjectReplaceRecentsKey];
	if (replaceRecents)
		[self setReplaceRecents:replaceRecents];
	else
		[self setReplaceRecents:[NSArray array]];
	
	// search scope identifier
	NSString *scopeIdentifier = [[[self project] projectPreferences] objectForKey:kWCProjectPrefsFindInProjectScopeIdentifierKey];
	if (scopeIdentifier)
		[_mgScopeBar setSelected:YES forItem:scopeIdentifier inGroup:0];
}

- (void)_toggleIsProcessingOnMainThread; {
	[self setIsProcessing:!_fipFlags.isProcessing];
}

- (void)_setProjectInfoStringOnMainThread:(NSString *)string; {
	[[self project] setProjectInfoString:string];
}

- (void)_expandOutlineViewOnMainThread; {
	[_outlineView expandItem:[_outlineView itemAtRow:0] expandChildren:YES];
}

- (void)_updateOutlineViewOnMainThread; {
	[_outlineView setNeedsDisplay:YES];
}

- (void)_updateFindProgressOnMainThread:(NSNumber *)currentProgress; {
	[self setFindProgress:[currentProgress floatValue]];
}

- (void)_addFindResultOnMainThread:(NSDictionary *)parentAndChildDict; {
	[[[parentAndChildDict objectForKey:@"parent"] mutableChildNodes] addObject:[parentAndChildDict objectForKey:@"child"]];
}

- (void)_findActionThreaded:(NSDictionary *)attrs; {
	NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
	
	BOOL ignoreCase = [attrs boolForKey:@"ignoreCase"];
	BOOL useRegularExpressions = [attrs boolForKey:@"useRegularExpressions"];
	NSString *findString = [attrs objectForKey:@"findString"];
	NSArray *textFiles = [attrs objectForKey:@"textFiles"];
	WCFindResult *findResult = [attrs objectForKey:@"findResult"];
	NSUInteger numberOfFilesSearched = 0;
	NSUInteger numberOfResults = 0;
	
	if (useRegularExpressions) {
		const RKCompileOption options = (ignoreCase)?(RKCompileMultiline|RKCompileCaseless):(RKCompileMultiline);
		
		RKRegex *regex = [[[RKRegex alloc] initWithRegexString:findString options:options] autorelease];
		
		for (WCTextFile *tf in textFiles) {			
			if ([self shouldCancelFindAction]) {
				[findResult performSelectorOnMainThread:@selector(setResultMessage:) withObject:[NSString stringWithFormat:NSLocalizedString(@"Found \"%@\" - %u occurrences (find stopped)",@"Found \"%@\" - %u occurrences (find stopped)"), findString, numberOfResults] waitUntilDone:YES];
				[self performSelectorOnMainThread:@selector(_setProjectInfoStringOnMainThread:) withObject:[NSString stringWithFormat:NSLocalizedString(@"Found \"%@\" - %u occurrences (find stopped)",@"Found \"%@\" - %u occurrences (find stopped)"), findString, numberOfResults] waitUntilDone:YES];
				[self performSelectorOnMainThread:@selector(_toggleIsProcessingOnMainThread) withObject:nil waitUntilDone:YES];
				[self performSelectorOnMainThread:@selector(_updateFindProgressOnMainThread:) withObject:[NSNumber numberWithFloat:0] waitUntilDone:YES];
				[self performSelectorOnMainThread:@selector(_updateOutlineViewOnMainThread) withObject:nil waitUntilDone:YES];
				[self setShouldCancelFindAction:NO];
				[pool release];
				[NSThread exit];
				return;
			}
			
			[findResult performSelectorOnMainThread:@selector(setResultMessage:) withObject:[NSString stringWithFormat:NSLocalizedString(@"Found \"%@\" - %u occurrences",@"Found \"%@\" - %u occurrences"), findString, numberOfResults] waitUntilDone:YES];
			[self performSelectorOnMainThread:@selector(_setProjectInfoStringOnMainThread:) withObject:[NSString stringWithFormat:NSLocalizedString(@"Finding \"%@\" - %u occurrences",@"Finding \"%@\" - %u occurrences"), findString, numberOfResults] waitUntilDone:YES];
			
			NSString *string = [tf fileContents];
			RKEnumerator *matches = [[[RKEnumerator alloc] initWithRegex:regex string:string] autorelease];
			
			while ([matches nextRanges] != NULL) {
				NSRange foundRange = *[matches currentRanges];
				
				WCFindResult *cfr = nil;
				
				for (WCFindResult *fr in [findResult childNodes]) {
					if ([[fr textFile] isEqualTo:tf]) {
						cfr = fr;
						break;
					}
				}
				
				if (!cfr) {
					cfr = [WCFindResult findResultWithType:WCFRTypeFile textFile:tf range:NSMakeRange(0, 0) message:nil];
					[self performSelectorOnMainThread:@selector(_addFindResultOnMainThread:) withObject:[NSDictionary dictionaryWithObjectsAndKeys:findResult, @"parent", cfr, @"child", nil] waitUntilDone:YES];
				}
				
				NSUInteger index, lineNumber;
				for (index = 0, lineNumber = 0; index <= foundRange.location; lineNumber++) {
					index = NSMaxRange([string lineRangeForRange:NSMakeRange(index, 0)]);	
				}
				
				WCFindResult *fr = [WCFindResult findResultWithType:WCFRTypeMatch textFile:tf range:foundRange message:nil];
				
				NSMutableString *displayString = [NSMutableString string];
				NSString *lineNumberString = [NSString stringWithFormat:@"%@:%u\t",[tf name],lineNumber];
				[displayString appendString:lineNumberString];
				NSRange lineRange = [string lineRangeForRange:foundRange];
				[displayString appendString:[NSString replaceAllNewLineCharactersWithSymbolInString:[string substringWithRange:lineRange]]];
				NSMutableAttributedString *attrString = [[[NSMutableAttributedString alloc] initWithString:displayString attributes:[NSDictionary dictionaryWithObject:[NSFont systemFontOfSize:11.0] forKey:NSFontAttributeName]] autorelease];
				NSMutableParagraphStyle *style = [[[NSParagraphStyle defaultParagraphStyle] mutableCopy] autorelease];
				[style setLineBreakMode:NSLineBreakByTruncatingMiddle];
				[attrString addAttribute:NSParagraphStyleAttributeName value:style range:NSMakeRange(0, [displayString length])];
				[attrString applyFontTraits:NSBoldFontMask range:NSMakeRange(foundRange.location-lineRange.location+[lineNumberString length], foundRange.length)];
				
				[fr setResultAttributedString:attrString];
				
				numberOfResults++;
				[self performSelectorOnMainThread:@selector(_addFindResultOnMainThread:) withObject:[NSDictionary dictionaryWithObjectsAndKeys:cfr, @"parent", fr, @"child", nil] waitUntilDone:YES];
			}
			
			[self performSelectorOnMainThread:@selector(_updateFindProgressOnMainThread:) withObject:[NSNumber numberWithFloat:(CGFloat)++numberOfFilesSearched] waitUntilDone:YES];
		}
	}
	else {
		for (WCTextFile *tf in textFiles) {
			if ([self shouldCancelFindAction]) {
				[findResult performSelectorOnMainThread:@selector(setResultMessage:) withObject:[NSString stringWithFormat:NSLocalizedString(@"Found \"%@\" - %u occurrences (find stopped)",@"Found \"%@\" - %u occurrences (find stopped)"), findString, numberOfResults] waitUntilDone:YES];
				[self performSelectorOnMainThread:@selector(_setProjectInfoStringOnMainThread:) withObject:[NSString stringWithFormat:NSLocalizedString(@"Found \"%@\" - %u occurrences (find stopped)",@"Found \"%@\" - %u occurrences (find stopped)"), findString, numberOfResults] waitUntilDone:YES];
				[self performSelectorOnMainThread:@selector(_toggleIsProcessingOnMainThread) withObject:nil waitUntilDone:YES];
				[self performSelectorOnMainThread:@selector(_updateFindProgressOnMainThread:) withObject:[NSNumber numberWithFloat:0] waitUntilDone:YES];
				[self performSelectorOnMainThread:@selector(_updateOutlineViewOnMainThread) withObject:nil waitUntilDone:YES];
				[self setShouldCancelFindAction:NO];
				[pool release];
				[NSThread exit];
				return;
			}
			
			[findResult performSelectorOnMainThread:@selector(setResultMessage:) withObject:[NSString stringWithFormat:NSLocalizedString(@"Found \"%@\" - %u occurrences",@"Found \"%@\" - %u occurrences"), findString, numberOfResults] waitUntilDone:YES];
			[self performSelectorOnMainThread:@selector(_setProjectInfoStringOnMainThread:) withObject:[NSString stringWithFormat:NSLocalizedString(@"Finding \"%@\" - %u occurrences",@"Finding \"%@\" - %u occurrences"), findString, numberOfResults] waitUntilDone:YES];
			
			NSString *string = [tf fileContents];
			NSInteger length = [string length];
			NSRange searchRange = NSMakeRange(0, length);
			NSRange foundRange;
			NSInteger startLocation = searchRange.location;
			
			while (startLocation < length) {
				if (ignoreCase) {
					foundRange = [string rangeOfString:findString options:NSCaseInsensitiveSearch range:NSMakeRange(startLocation, NSMaxRange(searchRange)-startLocation)];
				}
				else {
					foundRange = [string rangeOfString:findString options:NSLiteralSearch range:NSMakeRange(startLocation, NSMaxRange(searchRange)-startLocation)];
				}
				
				if (foundRange.location == NSNotFound)
					break;
				
				WCFindResult *cfr = nil;
				
				for (WCFindResult *fr in [[self findResult] childNodes]) {
					if ([[fr textFile] isEqualTo:tf]) {
						cfr = fr;
						break;
					}
				}
				
				if (!cfr) {
					cfr = [WCFindResult findResultWithType:WCFRTypeFile textFile:tf range:NSMakeRange(0, 0) message:nil];
					[self performSelectorOnMainThread:@selector(_addFindResultOnMainThread:) withObject:[NSDictionary dictionaryWithObjectsAndKeys:findResult, @"parent", cfr, @"child", nil] waitUntilDone:YES];
				}
				
				NSUInteger index, lineNumber;
				for (index = 0, lineNumber = 0; index <= foundRange.location; lineNumber++) {
					index = NSMaxRange([string lineRangeForRange:NSMakeRange(index, 0)]);	
				}
				
				WCFindResult *fr = [WCFindResult findResultWithType:WCFRTypeMatch textFile:tf range:foundRange message:nil];
				
				NSMutableString *displayString = [NSMutableString string];
				NSString *lineNumberString = [NSString stringWithFormat:@"%@:%u\t",[tf name],lineNumber];
				[displayString appendString:lineNumberString];
				NSRange lineRange = [string lineRangeForRange:foundRange];
				[displayString appendString:[NSString replaceAllNewLineCharactersWithSymbolInString:[string substringWithRange:lineRange]]];
				NSMutableAttributedString *attrString = [[[NSMutableAttributedString alloc] initWithString:displayString attributes:[NSDictionary dictionaryWithObject:[NSFont systemFontOfSize:11.0] forKey:NSFontAttributeName]] autorelease];
				NSMutableParagraphStyle *style = [[[NSParagraphStyle defaultParagraphStyle] mutableCopy] autorelease];
				[style setLineBreakMode:NSLineBreakByTruncatingMiddle];
				[attrString addAttribute:NSParagraphStyleAttributeName value:style range:NSMakeRange(0, [displayString length])];
				[attrString applyFontTraits:NSBoldFontMask range:NSMakeRange(foundRange.location-lineRange.location+[lineNumberString length], foundRange.length)];
				
				[fr setResultAttributedString:attrString];
				
				numberOfResults++;
				
				[self performSelectorOnMainThread:@selector(_addFindResultOnMainThread:) withObject:[NSDictionary dictionaryWithObjectsAndKeys:cfr, @"parent", fr, @"child", nil] waitUntilDone:YES];
				
				startLocation = NSMaxRange(foundRange);
			}
			
			[self performSelectorOnMainThread:@selector(_updateFindProgressOnMainThread:) withObject:[NSNumber numberWithFloat:(CGFloat)++numberOfFilesSearched] waitUntilDone:YES];
		}
	}
	
	[self setNumberOfFindResults:numberOfResults];
	
	if (numberOfResults)
		[self setFindLastString:findString];
	
	[findResult performSelectorOnMainThread:@selector(setResultMessage:) withObject:[NSString stringWithFormat:NSLocalizedString(@"Found \"%@\" - %u occurrences",@"Found \"%@\" - %u occurrences"), findString, numberOfResults] waitUntilDone:YES];
	
	[self performSelectorOnMainThread:@selector(_toggleIsProcessingOnMainThread) withObject:nil waitUntilDone:YES];
	
	[self performSelectorOnMainThread:@selector(_setProjectInfoStringOnMainThread:) withObject:[NSString stringWithFormat:NSLocalizedString(@"Found \"%@\" - %u occurrences",@"Found \"%@\" - %u occurrences"), findString, numberOfResults] waitUntilDone:YES];
	
	[self performSelectorOnMainThread:@selector(_expandOutlineViewOnMainThread) withObject:nil waitUntilDone:YES];
	
	[pool release];
	[NSThread exit];
}
#pragma mark Callbacks
- (void)_replaceAlertDidEnd:(NSAlert *)alert code:(NSInteger)code info:(void *)info {
	if (code != NSAlertDefaultReturn)
		return;
	
	[self _performReplaceAction];
}
#pragma mark IBActions
- (IBAction)_outlineViewDoubleAction:(id)sender {
	NSTreeNode *node = [[[_outlineView dataSource] selectedNodes] firstObject];
	WCFindResult *fr = [node representedObject];
	
	if ([fr resultType] == WCFRTypeMatch) {
		WCTextFile *tf = [fr textFile];
		
		if (!tf)
			return;
		
		[[self project] addTabViewItemForTextFile:tf inTabViewContext:self];
		
		WCTextView *tv = [self currentTextView];
		
		[tv setSelectedRange:[fr resultRange]];
		[tv scrollRangeToVisible:[fr resultRange]];
		[tv showFindIndicatorForRange:[fr resultRange]];
	}
	else {
		if ([_outlineView isItemExpanded:node])
			[_outlineView collapseItem:node];
		else
			[_outlineView expandItem:node];
	}
}

- (IBAction)_textViewSymbolsPopUpButtonAction:(id)sender {
	WCSymbol *symbol = [sender representedObject];
	
	WCTextView *tv = [self currentTextView];
	NSString *string = [tv string];
	NSRange range = [string lineRangeForRange:NSMakeRange([[symbol textFile] lineStartIndexForLineNumber:[symbol lineNumber]], 0)];
	
	range = [string rangeOfString:[symbol name] options:NSLiteralSearch range:range];
	
	[tv setSelectedRange:range];
	[tv scrollRangeToVisible:[tv selectedRange]];
	[tv showFindIndicatorForRange:[tv selectedRange]];
}
@end
